Skip to content

Add Stop, Restart, and Force Rebuild menu items to Tsunami view#2483

Merged
sawka merged 6 commits intomainfrom
copilot/add-settings-menu-items
Oct 27, 2025
Merged

Add Stop, Restart, and Force Rebuild menu items to Tsunami view#2483
sawka merged 6 commits intomainfrom
copilot/add-settings-menu-items

Conversation

Copy link
Contributor

Copilot AI commented Oct 27, 2025

Adds settings menu items to the Tsunami app view for stopping and restarting the controller, with support for forcing rebuilds that bypass the build cache.

Changes

  • Added three menu items to TsunamiViewModel.getSettingsMenuItems():

    • "Stop WaveApp" - stops the running tsunami controller using ControllerStopCommand
    • "Restart WaveApp" - stops the controller, waits 300ms, then resyncs to restart it (uses existing cache if available)
    • "Restart WaveApp and Force Rebuild" - triggers ControllerResyncCommand with forcerestart: true (bypasses build cache per line 228 in tsunamicontroller.go)
  • Refactored controller resync logic - extracted common code into doControllerResync() helper with optional triggerRestart parameter to handle both user-initiated restarts and automatic mount sync

  • Implemented proper restart behavior - "Restart WaveApp" now performs a stop → wait → resync sequence to ensure the controller actually restarts instead of just resyncing to the already-running state

Implementation Note

No backend changes required. The existing CommandControllerResyncData.ForceRestart flag already controls cache bypass behavior in the tsunami controller's build logic, and ControllerStopCommand is used for stopping the controller.

Original prompt

for tsunami app views... see tsunami.tsx (in frontend/app/view/tsunami) ... i need to add a settingsmenuitem... for restarting the tsunami controller (basically restarting the app). and then a second menu item that's going to be Restart and Force Rebuild ... that one will pass the force flag which should bypass the build cache.

that's implemented in tsunamicontroller.go ...

not sure if we need a new RPC call, but if we do:

we declare RPC calls in wshrpctypes.go and then implement server side RPC calls in wshserver.go... and then we run task generate to auto generate all the frontend/backend calling code...

from rules.md:

### Code Generation

- **TypeScript Types**: TypeScript types are automatically generated from Go types. After modifying Go types in `pkg/wshrpc/wshrpctypes.go`, run `task generate` to update the TypeScript type definitions in `frontend/types/gotypes.d.ts`.
- **Manual Edits**: Do not manually edit generated files like `frontend/types/gotypes.d.ts` or `frontend/app/store/wshclientapi.ts`. Instead, modify the source Go types and run `task generate`.

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@CLAassistant
Copy link

CLAassistant commented Oct 27, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ sawka
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI and others added 3 commits October 27, 2025 07:16
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Copilot AI changed the title [WIP] Add settings menu items for restarting tsunami controller Add Restart and Force Rebuild menu items to Tsunami view Oct 27, 2025
Copilot AI requested a review from sawka October 27, 2025 07:29
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Walkthrough

A centralized private helper method doControllerResync() was introduced to consolidate resync logic with optional restart triggering and state management. Three new public methods were added: stopController() to stop via RpcApi, restartController() as an async operation that stops, waits, and resyncs, and restartAndForceRebuild() for forced resync. The existing forceRestartController() was updated to use the new helper for backward compatibility. Additionally, tsunami-specific actions (Stop WaveApp, Restart WaveApp, Restart WaveApp and Force Rebuild) were added to the settings menu.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

  • Async flow verification: Ensure restartController() properly handles the timing between stop, wait, and resync operations
  • Error handling consistency: Verify error handling is comprehensive across stopController(), restartController(), and the centralized doControllerResync() helper
  • State management: Confirm restart state management in the helper correctly propagates through all call paths
  • Menu integration: Validate the new tsunami-specific menu items are positioned correctly and trigger the appropriate controller methods
  • Backward compatibility: Confirm forceRestartController() maintains existing behavior through the refactored helper

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Add Stop, Restart, and Force Rebuild menu items to Tsunami view" directly corresponds to the main changes in the changeset. The raw summary and PR description confirm that the primary change is the expansion of the settings menu with three new tsunami-specific actions. The title is specific and clear, accurately capturing the core purpose of the changes without unnecessary verbosity or vague language. While refactoring of the resync logic is also present, the title appropriately focuses on the primary user-facing change, which is acceptable per the evaluation criteria.
Description Check ✅ Passed The pull request description is clearly related to the changeset, providing accurate information about the three new menu items being added (Stop WaveApp, Restart WaveApp, and Restart WaveApp and Force Rebuild) and the refactoring of controller resync logic. The description includes meaningful detail about what each change accomplishes, including the stop-wait-resync sequence for proper restart behavior and an implementation note clarifying that no backend changes are required. This level of clarity and specificity exceeds the lenient threshold for passing this check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/add-settings-menu-items

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sawka
Copy link
Member

sawka commented Oct 27, 2025

@copilot ok, so just tested. the restart and force rebuild works great! but the restart does not actually trigger a restart -- because it is re-syncing... the synced state is running and so nothing happens. i'm thinking we should expose a "Stop WaveApp" item as well. You can use:

    // command "controllerstop" [call]
    ControllerStopCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
        return client.wshRpcCall("controllerstop", data, opts);
    }

and then for restart, it is just a stop, wait, and then call resync (which should restart it).

Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 27, 2025

@copilot ok, so just tested. the restart and force rebuild works great! but the restart does not actually trigger a restart -- because it is re-syncing... the synced state is running and so nothing ...

Added "Stop WaveApp" menu item and fixed restart logic. The "Restart WaveApp" now properly stops the controller, waits 300ms, then resyncs to restart it. Commit: 9cee89a

Menu items now:

  • Stop WaveApp - stops the running controller
  • Restart WaveApp - stop → wait → resync (uses cache if available)
  • Restart WaveApp and Force Rebuild - forcerestart with cache bypass

Copilot AI changed the title Add Restart and Force Rebuild menu items to Tsunami view Add Stop, Restart, and Force Rebuild menu items to Tsunami view Oct 27, 2025
@sawka sawka marked this pull request as ready for review October 27, 2025 18:05
@sawka
Copy link
Member

sawka commented Oct 27, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
frontend/app/view/tsunami/tsunami.tsx (3)

90-93: Make resyncController async and await helper

If doControllerResync returns a Promise, await it for consistent flow and error bubbling.

-    resyncController() {
-        this.doControllerResync(false, "resync", false);
-    }
+    async resyncController() {
+        await this.doControllerResync(false, "resync", false);
+    }

160-169: String-based filtering is brittle; prefer identifiers/roles

Filtering menu items by substring risks false positives and i18n issues. If ContextMenuItem has an id/role, filter on that instead. Otherwise, consider guarding only known labels or using a source flag from super.


170-189: Conditionally enable items and avoid double separators

  • Disable “Stop” when not running; “Restart” can be enabled always.
  • Insert separator only if there are remaining items and the next item isn’t already a separator.
-        const tsunamiItems: ContextMenuItem[] = [
+        const status = globalStore.get(this.shellProcFullStatus)?.shellprocstatus;
+        const running = status === "running";
+        const tsunamiItems: ContextMenuItem[] = [
             {
                 label: "Stop WaveApp",
-                click: () => this.stopController(),
+                click: () => this.stopController(),
+                enabled: !!running,
             },
             {
                 label: "Restart WaveApp",
-                click: () => this.restartController(),
+                click: () => this.restartController(),
             },
             {
                 label: "Restart WaveApp and Force Rebuild",
-                click: () => this.restartAndForceRebuild(),
+                click: () => this.restartAndForceRebuild(),
             },
-            {
-                type: "separator",
-            },
         ];
-
-        return [...tsunamiItems, ...filteredItems];
+        const needsSep = filteredItems.length > 0;
+        return needsSep
+            ? [...tsunamiItems, { type: "separator" }, ...filteredItems]
+            : [...tsunamiItems, ...filteredItems];
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 755d978 and 9cee89a.

📒 Files selected for processing (1)
  • frontend/app/view/tsunami/tsunami.tsx (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: merge-gatekeeper

Comment on lines +75 to 88
private doControllerResync(forceRestart: boolean, logContext: string, triggerRestart: boolean = true) {
if (triggerRestart) {
if (globalStore.get(this.isRestarting)) {
return;
}
this.triggerRestartAtom();
}
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: false,
forcerestart: forceRestart,
});
prtn.catch((e) => console.log("error controller resync", e));
prtn.catch((e) => console.log(`error controller resync (${logContext})`, e));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Return a Promise from doControllerResync and propagate errors

Callers can’t await the resync; errors are only logged. Make it async, return the underlying Promise, and rethrow to let callers handle.

-    private doControllerResync(forceRestart: boolean, logContext: string, triggerRestart: boolean = true) {
+    private async doControllerResync(
+        forceRestart: boolean,
+        logContext: string,
+        triggerRestart: boolean = true
+    ): Promise<void> {
         if (triggerRestart) {
             if (globalStore.get(this.isRestarting)) {
                 return;
             }
             this.triggerRestartAtom();
         }
-        const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
+        return RpcApi.ControllerResyncCommand(TabRpcClient, {
             tabid: globalStore.get(atoms.staticTabId),
             blockid: this.blockId,
             forcerestart: forceRestart,
-        });
-        prtn.catch((e) => console.log(`error controller resync (${logContext})`, e));
+        }).catch((e) => {
+            console.log(`error controller resync (${logContext})`, e);
+            throw e;
+        });
     }

If RpcApi supports RpcOpts timeouts, consider adding a bounded timeout. Please confirm with:


🏁 Script executed:

#!/bin/bash
rg -n -C3 "ControllerResyncCommand\\s*\\(" \
  && rg -n -C3 "type\\s+RpcOpts|interface\\s+RpcOpts|export\\s+type\\s+RpcOpts"

Length of output: 5675


Return a Promise from doControllerResync and propagate errors

The underlying RpcApi.ControllerResyncCommand already returns Promise<void>, but this method doesn't return or await it. Make the method async to return the Promise and rethrow errors so callers can handle them. Additionally, callers should pass RpcOpts with a timeout for robustness.

-    private doControllerResync(forceRestart: boolean, logContext: string, triggerRestart: boolean = true) {
+    private async doControllerResync(
+        forceRestart: boolean,
+        logContext: string,
+        triggerRestart: boolean = true
+    ): Promise<void> {
         if (triggerRestart) {
             if (globalStore.get(this.isRestarting)) {
                 return;
             }
             this.triggerRestartAtom();
         }
-        const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
+        return RpcApi.ControllerResyncCommand(TabRpcClient, {
             tabid: globalStore.get(atoms.staticTabId),
             blockid: this.blockId,
             forcerestart: forceRestart,
-        });
-        prtn.catch((e) => console.log(`error controller resync (${logContext})`, e));
+        }).catch((e) => {
+            console.log(`error controller resync (${logContext})`, e);
+            throw e;
+        });
     }

Update all call sites (e.g., restartAndForceRebuild, restartController) to await the result. Consider adding rtopts parameter with timeout.

🤖 Prompt for AI Agents
In frontend/app/view/tsunami/tsunami.tsx around lines 75 to 88,
doControllerResync currently calls RpcApi.ControllerResyncCommand but neither
returns nor awaits its Promise; change the method signature to async and have it
return (or await and return) the RpcApi.ControllerResyncCommand Promise so
errors propagate to callers; rethrow errors instead of just logging (or let the
Promise reject) so callers can handle them; add an optional rtopts: RpcOpts
parameter (with a default timeout) and pass it into ControllerResyncCommand;
finally update all call sites (e.g., restartAndForceRebuild, restartController)
to await doControllerResync and handle/rethrow errors as appropriate.

Comment on lines +94 to +98
stopController() {
const prtn = RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
prtn.catch((e) => console.log("error stopping controller", e));
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Stop should be awaitable; verify payload shape

Expose an async API and rethrow on failure so callers (and tests) can react. Also verify whether stop expects blockId or an object with tabid/blockid.

-    stopController() {
-        const prtn = RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
-        prtn.catch((e) => console.log("error stopping controller", e));
-    }
+    async stopController() {
+        try {
+            await RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
+        } catch (e) {
+            console.log("error stopping controller", e);
+            throw e;
+        }
+    }

Run to confirm expected signature:


🏁 Script executed:

#!/bin/bash
# Locate definition and usages to confirm the data argument.
rg -n -C3 "ControllerStopCommand\\s*\\("

Length of output: 3317


Make stopController() async and properly handle errors to allow caller reaction

The verification confirms the concern: RpcApi.ControllerStopCommand returns Promise<void> and is already awaitable, but the current implementation silently swallows errors. Notably, line 106 in the same file already demonstrates the correct pattern with await RpcApi.ControllerStopCommand(...) in a try block. The payload shape is correct—this.blockId is a string as expected by the backend.

Apply the suggested refactor to match the established pattern elsewhere in the codebase:

-    stopController() {
-        const prtn = RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
-        prtn.catch((e) => console.log("error stopping controller", e));
-    }
+    async stopController() {
+        try {
+            await RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
+        } catch (e) {
+            console.log("error stopping controller", e);
+            throw e;
+        }
+    }
🤖 Prompt for AI Agents
In frontend/app/view/tsunami/tsunami.tsx around lines 94 to 98, change
stopController to be an async method, await
RpcApi.ControllerStopCommand(TabRpcClient, this.blockId) inside a try block,
catch any thrown error, log it with contextual message, and rethrow the error so
the caller can react to failures (matching the pattern used at line 106).

Comment on lines +99 to +118
async restartController() {
if (globalStore.get(this.isRestarting)) {
return;
}
this.triggerRestartAtom();
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: true,
});
prtn.catch((e) => console.log("error controller resync (force restart)", e));
try {
// Stop the controller first
await RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
// Wait a bit for the controller to fully stop
await new Promise((resolve) => setTimeout(resolve, 300));
// Then resync to restart it
await RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: false,
});
} catch (e) {
console.log("error restarting controller", e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicate restart risk and brittle 300ms UI gating; use best‑effort stop and an in‑flight guard

isRestarting resets after 300ms, allowing overlapping restarts and misleading UI. Add a restartInFlight guard, keep UI pulse via triggerRestartAtom, proceed to resync even if stop says “already stopped,” and allow force rebuild reuse.

-    async restartController() {
-        if (globalStore.get(this.isRestarting)) {
-            return;
-        }
-        this.triggerRestartAtom();
-        try {
-            // Stop the controller first
-            await RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
-            // Wait a bit for the controller to fully stop
-            await new Promise((resolve) => setTimeout(resolve, 300));
-            // Then resync to restart it
-            await RpcApi.ControllerResyncCommand(TabRpcClient, {
-                tabid: globalStore.get(atoms.staticTabId),
-                blockid: this.blockId,
-                forcerestart: false,
-            });
-        } catch (e) {
-            console.log("error restarting controller", e);
-        }
-    }
+    async restartController(forceRebuild: boolean = false) {
+        if (this.restartInFlight) return;
+        this.restartInFlight = true;
+        this.triggerRestartAtom();
+        try {
+            // Best‑effort stop; continue even if already stopped.
+            try {
+                await RpcApi.ControllerStopCommand(TabRpcClient, this.blockId);
+            } catch (e) {
+                console.log("controller stop failed (continuing to resync)", e);
+            }
+            await new Promise((resolve) => setTimeout(resolve, 300));
+            await RpcApi.ControllerResyncCommand(TabRpcClient, {
+                tabid: globalStore.get(atoms.staticTabId),
+                blockid: this.blockId,
+                forcerestart: forceRebuild,
+            });
+        } catch (e) {
+            console.log("error restarting controller", e);
+        } finally {
+            this.restartInFlight = false;
+        }
+    }

Add this field to the class (outside this hunk):

// Near other fields in TsunamiViewModel
private restartInFlight = false;
🤖 Prompt for AI Agents
In frontend/app/view/tsunami/tsunami.tsx around lines 99 to 118, the restart
flow uses a brittle 300ms UI gating and can allow overlapping restarts; add a
private restartInFlight = false field on the TsunamiViewModel and update
restartController to return early if restartInFlight is true, set
restartInFlight = true at start and reset it in a finally block, keep calling
triggerRestartAtom() to preserve the UI pulse, attempt
RpcApi.ControllerStopCommand but treat failures (including “already stopped”) as
non-fatal and proceed to call RpcApi.ControllerResyncCommand regardless of stop
result, and remove the fixed 300ms timeout so you wait only for the stop Promise
(best‑effort) before resyncing.

Comment on lines +120 to +123
restartAndForceRebuild() {
this.doControllerResync(true, "force rebuild");
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Menu label says “Restart … and Force Rebuild” but code doesn’t stop first

Current implementation is a resync-only path. Align behavior with the label by reusing restartController(true).

-    restartAndForceRebuild() {
-        this.doControllerResync(true, "force rebuild");
-    }
+    async restartAndForceRebuild() {
+        await this.restartController(true);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
restartAndForceRebuild() {
this.doControllerResync(true, "force rebuild");
}
async restartAndForceRebuild() {
await this.restartController(true);
}
🤖 Prompt for AI Agents
In frontend/app/view/tsunami/tsunami.tsx around lines 120 to 123, the
restartAndForceRebuild method currently calls doControllerResync(true, "force
rebuild") which only resyncs; change it to call restartController(true) so the
controller is stopped and restarted with the force-rebuild flag. Replace the
doControllerResync call with restartController(true), preserving any
return/await behavior and updating any callers if the method signature/return
differs.

@sawka sawka merged commit 80c25de into main Oct 27, 2025
6 of 8 checks passed
@sawka sawka deleted the copilot/add-settings-menu-items branch October 27, 2025 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants